package listener

import (
	"go.uber.org/zap"
	"gse/transport"
	gseTErr "gse/transport/errors"
	"gse/transport/udp/connection"
	"gse/transport/udp/packet"
	"net"
	"sync"
)

const DEFAULT_BUFFER_SIZE = 3

// DEFAULT_BUFFER_SORT_DELAY in milliseconds.
const DEFAULT_BUFFER_SORT_DELAY = 20

const DefaultReadDataBuffPoolSize = 5

type UDPListner struct {
	conn               *net.UDPConn
	readDataBufferPool chan []byte
	logger             *zap.Logger
	// the decoded packet chan.
	packetChan             chan packet.UdpPacket
	obPacketChan           chan *packet.PacketHeader
	connectionManager      *ConnectionManager
	connectionIncomingChan chan *connection.Connection
	closed                 chan bool
	wg                     *sync.WaitGroup
	receiver               *connection.UDPPacketReceiver
	cfg                    *UDPListnerConfig
}

type UDPListnerConfig struct {
	*ConnectionManagerConfig
	ConnectionReadBufferSize    int
	ConnectionWriteBufferSize   int
	UDPDataPacketChanSize       int
	UDPAckPacketChanSize        int
	UDPPacketReadBufferPoolSize int
	UDPPacketReadBufferSize     uint16
	ConnectBacklog              int
	Logger                      *zap.Logger
}

func NewDefaultUDPListnerConfig() *UDPListnerConfig {
	logger, _ := zap.NewDevelopment()
	cfg := &UDPListnerConfig{
		ConnectionManagerConfig: &ConnectionManagerConfig{
			UDPConnectionConfig: connection.NewDefaultUDPConnectionConfig(),
			Logger:              logger,
		},
		ConnectionReadBufferSize:    1024 * 1024,
		ConnectionWriteBufferSize:   1024 * 1024,
		UDPDataPacketChanSize:       10000,
		UDPAckPacketChanSize:        10000,
		UDPPacketReadBufferPoolSize: DefaultReadDataBuffPoolSize,
		UDPPacketReadBufferSize:     packet.PacketSize,
		ConnectBacklog:              10000,
		Logger:                      logger,
	}
	cfg.UDPConnectionConfig.Logger = logger
	return cfg
}

func NewUDPListner(conn *net.UDPConn, cfg *UDPListnerConfig) transport.Transport {
	t := &UDPListner{
		conn:                   conn,
		readDataBufferPool:     make(chan []byte, cfg.UDPPacketReadBufferPoolSize),
		logger:                 cfg.Logger,
		obPacketChan:           make(chan *packet.PacketHeader, cfg.UDPAckPacketChanSize),
		packetChan:             make(chan packet.UdpPacket, cfg.UDPDataPacketChanSize),
		connectionIncomingChan: make(chan *connection.Connection, cfg.ConnectBacklog),
		closed:                 make(chan bool),
		wg:                     new(sync.WaitGroup),
		cfg:                    cfg,
	}
	t.connectionManager = NewConnectionManager(
		cfg.ConnectionManagerConfig,
		t.conn,
	)
	t.conn.SetReadBuffer(cfg.ConnectionReadBufferSize)
	t.conn.SetWriteBuffer(cfg.ConnectionWriteBufferSize)
	t.receiver = connection.NewUDPPacketReceiver(t.closed, packet.PacketSize, t.readDataBufferPool, t.conn, t.packetChan, t.obPacketChan, t.logger)
	go t.receiver.RecvUDPPacket()

	t.wg.Add(1)
	go t.distribute()
	return t
}

func (t *UDPListner) Accept() (transport.Connection, error) {
	for {
		select {
		case <-t.closed:
			return nil, gseTErr.ServerShutdown
		case conn := <-t.connectionIncomingChan:
			return conn, nil
		}
	}
}

func (t *UDPListner) Shutdown() {
	close(t.closed)
	t.conn.Close()
	t.wg.Wait()
}

func (t *UDPListner) SendTo(data []byte, addr net.Addr) (n int, err error) {
	return t.conn.WriteTo(data, addr)
}

// distribute distributes incoming udp packets to the client connection handling routines
func (t *UDPListner) distribute() {
	defer t.wg.Done()
	var dataPacket packet.UdpPacket
	for {
		select {
		case dataPacket = <-t.packetChan:
		case dataPacket = <-t.obPacketChan:
		}

		conn, created, err := t.connectionManager.GetConnection(dataPacket.GetRemoteAddr())
		if created {
			t.connectionIncomingChan <- conn
		} else if err != nil {
			t.logger.Warn("failed to create connection for incoming packet..", zap.String("remote_addr", dataPacket.GetRemoteAddr().String()), zap.Error(err))
			continue
		}

		err = conn.AllocateUDPPacket(dataPacket)
		if err != nil {
			t.logger.Warn("failed to distribute packet.", zap.String("remote_addr", dataPacket.GetRemoteAddr().String()), zap.Error(err))
		}
	}
}
